/*
 * Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.openmuc.openiec61850;

import java.io.IOException;
import java.net.InetAddress;

import javax.net.SocketFactory;

import org.openmuc.josistack.ClientAcseSap;

/**
 * The <code>ClientSap</code> class represents the IEC 61850 service access point for client applications. A client
 * application that wants to connect to a server should first create an instance of <code>ClientSap</code>. Next all the
 * necessary configuration parameters can be set. Finally the <code>associate</code> function is called to connect to
 * the server. An instance of <code>ClientSap</code> can be used to create an unlimited number of associations. Changing
 * the parameters of a ClientSap has no affect on associations that have already been created.
 */
public final class ClientSap {

    static final int MINIMUM_MMS_PDU_SIZE = 64;
    private static final int MAXIMUM_MMS_PDU_SIZE = 65000;

    private static final byte[] DEFAULT_TSEL_LOCAL = new byte[] { 0, 0 };
    private static final byte[] DEFAULT_TSEL_REMOTE = new byte[] { 0, 1 };
    private static final int DEFAUTL_TPDU_SIZE_PARAMETER = 10; // size = 1024

    private int proposedMaxMmsPduSize = 65000;
    private final int proposedMaxServOutstandingCalling = 5;
    private final int proposedMaxServOutstandingCalled = 5;
    private final int proposedDataStructureNestingLevel = 10;
    private byte[] servicesSupportedCalling = new byte[] { (byte) 0xee, 0x1c, 0, 0, 0x04, 0x08, 0, 0, 0x79, (byte) 0xef,
            0x18 };

    private int messageFragmentTimeout = 10000;
    private int responseTimeout = 20000;

    private final ClientAcseSap acseSap;

    /**
     * Use this constructor to create a default client SAP.
     */
    public ClientSap() {
        acseSap = new ClientAcseSap();
        acseSap.tSap.tSelLocal = DEFAULT_TSEL_LOCAL;
        acseSap.tSap.tSelRemote = DEFAULT_TSEL_REMOTE;
        acseSap.tSap.setMaxTPDUSizeParam(DEFAUTL_TPDU_SIZE_PARAMETER);
    }

    /**
     * Use this constructor to create a client SAP that uses the given <code>SocketFactory</code> to connect to servers.
     * You could pass an SSLSocketFactory to enable SSL.
     * 
     * @param socketFactory
     *            the socket factory to construct the socket
     */
    public ClientSap(SocketFactory socketFactory) {
        acseSap = new ClientAcseSap(socketFactory);
        acseSap.tSap.tSelLocal = DEFAULT_TSEL_LOCAL;
        acseSap.tSap.tSelRemote = DEFAULT_TSEL_REMOTE;
        acseSap.tSap.setMaxTPDUSizeParam(DEFAUTL_TPDU_SIZE_PARAMETER);
    }

    /**
     * Sets the maximum MMS PDU size in bytes that the client association will support. The client proposes this value
     * to the server during association. If the server requires the use of a smaller maximum MMS PDU size, then the
     * smaller size will be accepted by the client. The default size is 65000.
     * 
     * @param size
     *            cannot be less than 64. The upper limit is 65000 so that segmentation at the lower transport layer is
     *            avoided. The Transport Layer's maximum PDU size is 65531.
     */
    public void setMaxMmsPduSize(int size) {
        if (size >= MINIMUM_MMS_PDU_SIZE && size <= MAXIMUM_MMS_PDU_SIZE) {
            proposedMaxMmsPduSize = size;
        }
        else {
            throw new IllegalArgumentException("maximum size is out of bound");
        }
    }

    /**
     * Gets the maximum MMS PDU size.
     * 
     * @return the maximum MMS PDU size.
     */
    public int getMaxMmsPduSize() {
        return proposedMaxMmsPduSize;
    }

    /**
     * Sets the SevicesSupportedCalling parameter. The given parameter is sent to the server but has no affect on the
     * functionality of this client.
     * 
     * @param services
     *            the ServicesSupportedCalling parameter
     */
    public void setServicesSupportedCalling(byte[] services) {
        if (services.length != 11) {
            throw new IllegalArgumentException("The services parameter needs to be of lenth 11");
        }
        servicesSupportedCalling = services;
    }

    /**
     * Gets the ServicesSupportedCalling parameter.
     * 
     * @return the ServicesSupportedCalling parameter.
     */
    public byte[] getServicesSupportedCalling() {
        return servicesSupportedCalling;
    }

    /**
     * Sets the remote/called Session-Selector (S-SEL). The default remote S-SEL is byte[] { 0, 1 }.
     * 
     * @param sSelRemote
     *            the remote/called S-SEL.
     */
    public void setSSelRemote(byte[] sSelRemote) {
        acseSap.sSelRemote = sSelRemote;
    }

    /**
     * Sets the local/calling Session-Selector (S-SEL). The default local S-SEL is byte[] { 0, 1 }.
     * 
     * @param sSelLocal
     *            the local/calling S-SEL.
     */
    public void setSSelLocal(byte[] sSelLocal) {
        acseSap.sSelLocal = sSelLocal;
    }

    /**
     * Sets the remote/called Presentation-Selector (P-SEL). The default remote P-SEL is byte[] { 0, 0, 0, 1 }.
     * 
     * @param pSelRemote
     *            the remote/called P-SEL.
     */
    public void setPSelRemote(byte[] pSelRemote) {
        acseSap.pSelRemote = pSelRemote;
    }

    /**
     * Sets the local/calling Presentation-Selector (P-SEL). The default local P-SEL is byte[] { 0, 0, 0, 1 }.
     * 
     * @param pSelLocal
     *            the local/calling P-SEL.
     */
    public void setPSelLocal(byte[] pSelLocal) {
        acseSap.pSelLocal = pSelLocal;
    }

    /**
     * Sets the remote/called Transport-Selector (T-SEL). It is optionally transmitted in the OSI Transport Layer
     * connection request (CR). The default remote T-SEL is byte[] { 0, 1 }.
     * 
     * @param tSelRemote
     *            the remote/called T-SEL. If null the T-SEL will be omitted. No maximum size is defined by the
     *            standard.
     */
    public void setTSelRemote(byte[] tSelRemote) {
        acseSap.tSap.tSelRemote = tSelRemote;
    }

    /**
     * Sets the local/calling Transport-Selector (T-SEL). It is optionally transmitted in the OSI Transport Layer
     * connection request (CR). The default local T-SEL byte[] { 0, 0 }.
     * 
     * @param tSelLocal
     *            the local/calling T-SEL. If null the T-SEL will be omitted. No maximum size is defined by the
     *            standard.
     */
    public void setTSelLocal(byte[] tSelLocal) {
        acseSap.tSap.tSelLocal = tSelLocal;
    }

    /**
     * Set the maxTPDUSize. The default maxTPduSize is 65531 (see RFC 1006).
     * 
     * @param maxTPduSizeParam
     *            The maximum length is equal to 2^(maxTPduSizeParam) octets. Note that the actual TSDU size that can be
     *            transfered is equal to TPduSize-3. Default is 65531 octets (see RFC 1006), 7 &lt;= maxTPduSizeParam
     *            &lt;= 16, needs to be set before listening or connecting
     */
    public void setMaxTPduSizeParameter(int maxTPduSizeParam) {
        acseSap.tSap.setMaxTPDUSizeParam(maxTPduSizeParam);
    }

    /**
     * Sets the remote/called Application Process Title. The default value is int[] { 1, 1, 999, 1, 1 }
     * 
     * @param title
     *            the remote/called AP title.
     */
    public void setApTitleCalled(int[] title) {
        acseSap.setApTitleCalled(title);
    }

    /**
     * Sets the local/calling Application Process Title. The default value is int[] { 1, 1, 999, 1 }
     * 
     * @param title
     *            the local/calling AP title.
     */
    public void setApTitleCalling(int[] title) {
        acseSap.setApTitleCalling(title);
    }

    /**
     * Sets the remote/called Application Entity Qualifier. The default value is 12.
     * 
     * @param qualifier
     *            the remote/called AE Qualifier
     */
    public void setAeQualifierCalled(int qualifier) {
        acseSap.setAeQualifierCalled(qualifier);
    }

    /**
     * Sets the local/calling Application Entity Qualifier. The default value is 12.
     * 
     * @param qualifier
     *            the local/calling AE Qualifier
     */
    public void setAeQualifierCalling(int qualifier) {
        acseSap.setAeQualifierCalling(qualifier);
    }

    /**
     * Sets the default response timeout of the <code>ClientAssociation</code> that is created using this ClientSap.
     * 
     * @param timeout
     *            the response timeout in milliseconds. The default is 20000.
     */
    public void setResponseTimeout(int timeout) {
        responseTimeout = timeout;
    }

    /**
     * Sets the message fragment timeout. This is the timeout that the socket timeout is set to after the first byte of
     * a message has been received. A request function (e.g. setDataValues()) will throw an IOException if the socket
     * throws this timeout because the association/connection cannot recover from this kind of error.
     * 
     * @param timeout
     *            the timeout in milliseconds. The default is 10000.
     */
    public void setMessageFragmentTimeout(int timeout) {
        messageFragmentTimeout = timeout;
    }

    /**
     * Connects to the IEC 61850 MMS server at the given address and port and returns the resulting association object.
     *
     * @param address
     *            the address to connect to.
     * @param port
     *            the port to connect to. Usually the MMS port is 102.
     * @param authenticationParameter
     *            an optional authentication parameters that is transmitted. It will be omitted if equal to null.
     * @param reportListener
     *            the listener to be notified of incoming reports
     * @return the association object
     * @throws IOException
     *             if any kind of error occurs trying build up the association
     */
    public ClientAssociation associate(InetAddress address, int port, String authenticationParameter,
                                       ClientEventListener reportListener) throws IOException {

        return associate(address, port, authenticationParameter, null, -1, reportListener);
    }

    /**
     * Connects to the IEC 61850 MMS server at the given address and port and returns the resulting association object.
     *
     * @param address
     *            the address to connect to
     * @param port
     *            the port to connect to. Usually the MMS port is 102.
     * @param authenticationParameter
     *            an optional authentication parameters that is transmitted. It will be omitted if equal to null.
     * @param localAddr
     *            the local address to use
     * @param localPort
     *            the local port to use
     * @param reportListener
     *            the listener to be notified of incoming reports
     * @return the association object.
     * @throws IOException
     *             if any kind of error occurs trying build up the association
     */
    public ClientAssociation associate(InetAddress address, int port, String authenticationParameter,
                                       InetAddress localAddr, int localPort, ClientEventListener reportListener) throws IOException {

        if (port < 0 || port > 65535) {
            throw new IllegalArgumentException("invalid port");
        }

        if (address == null) {
            throw new IllegalArgumentException("address may not be null");
        }

        if (acseSap.sSelRemote == null) {
            throw new IllegalArgumentException("sSelRemote may not be null");
        }

        if (acseSap.sSelRemote.length != 2) {
            throw new IllegalArgumentException("sSelRemote lenght must be two");
        }

        if (acseSap.sSelLocal == null) {
            throw new IllegalArgumentException("sSelLocal may not be null");
        }

        if (acseSap.sSelLocal.length != 2) {
            throw new IllegalArgumentException("sSelLocal lenght must be two");
        }

        ClientAssociation clientAssociation = new ClientAssociation(address, port, localAddr, localPort,
                authenticationParameter, acseSap, proposedMaxMmsPduSize, proposedMaxServOutstandingCalling,
                proposedMaxServOutstandingCalled, proposedDataStructureNestingLevel, servicesSupportedCalling,
                responseTimeout, messageFragmentTimeout, reportListener);

        return clientAssociation;
    }

    /**
     * Connects to the IEC 61850 MMS server at the given address and port and returns the resulting association object.
     *
     * @param address
     *            the address to connect to.
     * @param port
     *            the port to connect to. Usually the MMS port is 102.
     * @param authenticationParameter
     *            an optional authentication parameters that is transmitted. It will be omitted if equal to null.
     *            the listener to be notified of incoming reports
     * @return the association object
     * @throws IOException
     *             if any kind of error occurs trying build up the association
     */
    public ClientAssociation associate(InetAddress address, int port, String authenticationParameter) throws IOException {

        return associate(address, port, authenticationParameter, null, -1);
    }
    /**
     * Connects to the IEC 61850 MMS server at the given address and port and returns the resulting association object.
     *
     * @param address
     *            the address to connect to
     * @param port
     *            the port to connect to. Usually the MMS port is 102.
     * @param authenticationParameter
     *            an optional authentication parameters that is transmitted. It will be omitted if equal to null.
     * @param localAddr
     *            the local address to use
     * @param localPort
     *            the local port to use
     * @return the association object.
     * @throws IOException
     *             if any kind of error occurs trying build up the association
     */
    public ClientAssociation associate(InetAddress address, int port, String authenticationParameter,
                                       InetAddress localAddr, int localPort) throws IOException {

        if (port < 0 || port > 65535) {
            throw new IllegalArgumentException("invalid port");
        }

        if (address == null) {
            throw new IllegalArgumentException("address may not be null");
        }

        if (acseSap.sSelRemote == null) {
            throw new IllegalArgumentException("sSelRemote may not be null");
        }

        if (acseSap.sSelRemote.length != 2) {
            throw new IllegalArgumentException("sSelRemote lenght must be two");
        }

        if (acseSap.sSelLocal == null) {
            throw new IllegalArgumentException("sSelLocal may not be null");
        }

        if (acseSap.sSelLocal.length != 2) {
            throw new IllegalArgumentException("sSelLocal lenght must be two");
        }

        ClientAssociation clientAssociation = new ClientAssociation(address, port, localAddr, localPort,
                authenticationParameter, acseSap, proposedMaxMmsPduSize, proposedMaxServOutstandingCalling,
                proposedMaxServOutstandingCalled, proposedDataStructureNestingLevel, servicesSupportedCalling,
                responseTimeout, messageFragmentTimeout, null);
        clientAssociation.setReportListener(clientAssociation);

        return clientAssociation;
    }
}
